package testrank.predictors;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import testrank.Conflict;
import testrank.ElementInfo;
import testrank.Predictor;
import testrank.TestManager;
import testrank.TestRankConstants;
import testrank.affinity.AffinityCalculator;
import testrank.affinity.SignatureParser;
import testrank.affinity.SourceFileAnalyzer;
import testrank.affinity.WordGroup;

/**
 * @author Hagai Cibulski
 *
 */
public class AffinityPredictor implements Predictor {

	private static final long serialVersionUID = 1L;

	private static final double LITERALS_WEIGHT = 0.05;

	private static final double METHOD_NAME_WORDS_FACTOR = 2.5; // must be > 0


	/**
	 * conflict[function][test]
	 */
	protected Map<Conflict, Double> conflictAffinities = new HashMap<Conflict, Double>();
	
	protected ExecutionCountPredictor countPredictor;

	// transient if NOT CURENTLY IN USE
	private transient Map<String, Double> totalExecutionCountDuringTest;
	
	public AffinityPredictor() {
	}
	
	/**
	 * @param predictor
	 */
	public AffinityPredictor(ExecutionCountPredictor countPredictor) {
		this.countPredictor = countPredictor;
	}

	@Override
	public void begin(String methodSignature, String testSignature) {
	}
	@Override
	public void end(String methodSignature, String testSignature) {
	}

	@Override
	public double rankFailure(String methodSignature, String testSignature) {
		Double affinity = conflictAffinities.get(new Conflict(methodSignature, testSignature));
		if (affinity == null)
			return 0;
		
		assert !Double.isNaN(affinity); 
		return affinity;
	}
	
	public void process() {
		AffinityCalculator calc = new AffinityCalculator(); 
		Set<Conflict> conflicts = new HashSet<Conflict>(countPredictor.conflicts.keySet());
		for (Conflict conflict : conflicts) {

			double groupAffinity = getConflictAffinity(calc, conflict, false);
			assert !Double.isNaN(groupAffinity); 
			
			conflictAffinities.put(conflict, groupAffinity);
			//System.out.println("conflict="+conflict+", wordsGroupAffinity="+wordsGroupAffinity+", literalsGroupAffinity="+literalGroupAffinity);
		}

		if (TestRankConstants.AFFINITY_ALT_NORMALIZATION == TestRankConstants.AffinityNormalization.ByAffSum
				|| TestRankConstants.AFFINITY_ALT_NORMALIZATION == TestRankConstants.AffinityNormalization.ByAffSumAvg
				|| TestRankConstants.AFFINITY_ALT_NORMALIZATION == TestRankConstants.AffinityNormalization.ByAffSumGeomAvg
				) {
			normalizeScores1(false, 0);
		}
		else if (TestRankConstants.AFFINITY_ALT_NORMALIZATION == TestRankConstants.AffinityNormalization.ByLogAffSum1) {
			normalizeScores1(true, 1.0);
		}
		else if (TestRankConstants.AFFINITY_ALT_NORMALIZATION == TestRankConstants.AffinityNormalization.ByLogAffSumE) {
			normalizeScores1(true, Math.E);
		}
		else if (TestRankConstants.AFFINITY_ALT_NORMALIZATION == TestRankConstants.AffinityNormalization.Recursive) {
			processScores();
		}
		
		//System.out.println("All Conflict Affinities:\n" + conflictAffinities);
		TestManager.saveAffinities();
	}

	public double getConflictAffinity(AffinityCalculator calc,
			Conflict conflict, boolean verbose) {
		String methodSignature = conflict.getMethodSignature();
		ElementInfo methodCodeElement = TestManager.locator().getCodeElementBySignature(methodSignature);
		if (methodCodeElement == null) {
			// this might happen if method is Java's default constructor
			// TODO see how to handle this case correctly
			// TODO make sure this doesn't happen in other cases also
			return 0.0;
		}
		WordGroup methodGroup = methodCodeElement.getWordGroup();
		
		String testSignature = conflict.getTestSignature();
		ElementInfo testCodeElement = TestManager.testLocator().getCodeElementBySignature(testSignature);
		WordGroup testGroup = testCodeElement.getWordGroup();

		// find all Covering Tests of Methods and all Covered Methods of Test 
		AffinityContext affinityContext = new AffinityContext(countPredictor, methodSignature, testSignature);
		
		SignatureParser methodSigParser = new SignatureParser(methodSignature);
		Set<String> methodNameWords = new TreeSet<String>(methodSigParser.getMethodNameWords());
		Set<String> methodNameLiterals = asSet(methodSigParser.getMethodNameLiteral());
		
		SignatureParser testSigParser = new SignatureParser(testSignature);
		Set<String> testNameWords = new TreeSet<String>(testSigParser.getMethodNameWords());
		Set<String> testNameLiterals = asSet(testSigParser.getMethodNameLiteral());
		
		Set<String> methodWords = filterCommonWords(methodGroup.getWords());
		Set<String> testWords = filterCommonWords(testGroup.getWords());

		// ensure preconditions for createWeightsMap; in case only the containing set was filtered
		methodWords.addAll(methodNameWords);
		testWords.addAll(testNameWords);
		
		double wordsGroupAffinity = calc.groupAffinity(
				createWeightsMap(methodWords, methodNameWords), 
				createWeightsMap(testWords, testNameWords), methodGroup, testGroup, 
				affinityContext, true, verbose);
		
		Set<String> methodLiterals = filterCommonWords(methodGroup.getLiterals());
		Set<String> testLiterals = filterCommonWords(testGroup.getLiterals());

		// ensure preconditions for createWeightsMap; in case only the containing set was filtered
		methodLiterals.addAll(methodNameLiterals);
		testLiterals.addAll(testNameLiterals);
		
		double literalGroupAffinity = calc.groupAffinity(
				createWeightsMap(methodLiterals, methodNameLiterals), 
				createWeightsMap(testLiterals, testNameLiterals), methodGroup, testGroup, 
				affinityContext, false, verbose);
		
		// words/literal combination
		double literalsWeight;
		if (TestRankConstants.AFFINITY_LITERALS_WEIGHT_STRAGEGY == 
			TestRankConstants.AffinityLiteralsWeightStrategy.Const) {
			literalsWeight = LITERALS_WEIGHT;
		}
		else if (TestRankConstants.AFFINITY_LITERALS_WEIGHT_STRAGEGY == 
			TestRankConstants.AffinityLiteralsWeightStrategy.BySum) {
			int nLiterals = methodLiterals.size() + testLiterals.size();
			int nWords = methodWords.size() + testWords.size();
			literalsWeight = (double)nLiterals / (double)(nLiterals + nWords);
			literalsWeight = literalsWeight / 10.0 + 0.05;
		}
		else if (TestRankConstants.AFFINITY_LITERALS_WEIGHT_STRAGEGY == 
			TestRankConstants.AffinityLiteralsWeightStrategy.ByMin) {
			int minLiterals = Math.min(methodLiterals.size(), testLiterals.size());
			int nLiterals = methodLiterals.size() + testLiterals.size();
			int nWords = methodWords.size() + testWords.size();
			literalsWeight = 2.0*minLiterals / (double)(nLiterals + nWords);
//			literalsWeight = literalsWeight / 5.0;
//			literalsWeight = literalsWeight / 10.0 + 0.05;
//			literalsWeight = literalsWeight / 10.0 + 0.10;
			literalsWeight = literalsWeight / 10.0 + 0.15;
		}
		else throw new IllegalStateException();
		
		double groupAffinity = (wordsGroupAffinity*(1.0-literalsWeight) + literalGroupAffinity*literalsWeight);
		
		if (verbose) {
			System.out.println("methodWords = " + methodWords);
			System.out.println("testWords = " + testWords);
			System.out.println("wordsGroupAffinity = " + wordsGroupAffinity);
			System.out.println();
			System.out.println("methodLiterals = " + methodLiterals);
			System.out.println("testLiterals = " + testLiterals);
			System.out.println("literalGroupAffinity = " + literalGroupAffinity);
			System.out.println();
			System.out.println("groupAffinity = " + groupAffinity);
		}
		
		return groupAffinity;
	}

	private Map<String, Double> createWeightsMap(Set<String> methodWords,
			Set<String> methodNameWords) {
		
		int n = methodWords.size();
		int m = methodNameWords.size();
		assert(n >= m);
		Map<String, Double> map = new HashMap<String, Double>();
		if (n > 0) {
			double b = 1.0 / (n + m * (METHOD_NAME_WORDS_FACTOR - 1));
			double a = METHOD_NAME_WORDS_FACTOR * b;

// method name affinity
//			double b = 0;
//			double a = 1.0 / m;
		
			for (String word : methodWords) {
				double weight = methodNameWords.contains(word) ? a : b;
				map.put(word, weight);
			}
		}
		return map;
	}

	private Set<String> filterCommonWords(Collection<String> words) {
		Set<String> informativeWords = new TreeSet<String>(words);
//		if (informativeWords.size() < 2)
//			return informativeWords;

		informativeWords.removeAll(SourceFileAnalyzer.getCommonWords());
		return informativeWords;
	}
	
	public static <T> Set<T> asSet(T e) {
		Set<T> s = new TreeSet<T>();
		s.add(e);
		return s;
	}
	
	@Override
	public String getName() {
		return "AFFIN";
	}
	
	/**
	 * calculates sum_i{score(m_i, t)}
	 * score'(m, t) = score(m, t) / sum_i{score(m_i, t)}
	 */
	private void normalizeScores1(boolean useLogarithm, double logarithmConst) {
		totalExecutionCountDuringTest = new HashMap<String, Double>();
		for (Conflict c : conflictAffinities.keySet()) {
			String testSignature = c.getTestSignature();
			Double sum = totalExecutionCountDuringTest.get(testSignature);
			if (sum == null) {
				sum = 0.0;
			}
			sum += conflictAffinities.get(c);
			totalExecutionCountDuringTest.put(testSignature, sum);
		}
		
		for (Conflict c : conflictAffinities.keySet()) {
			String testSignature = c.getTestSignature();
			Double sum = totalExecutionCountDuringTest.get(testSignature);
			if (useLogarithm) {
				sum = Math.log(sum + logarithmConst);
			}
			
			Double a = conflictAffinities.get(c);
			double a1;
			if (TestRankConstants.AFFINITY_ALT_NORMALIZATION == TestRankConstants.AffinityNormalization.ByAffSumAvg) {
				a1 = (a + (a/sum)) / 2.0; 
			}
			else if (TestRankConstants.AFFINITY_ALT_NORMALIZATION == TestRankConstants.AffinityNormalization.ByAffSumGeomAvg) {
				a1 = (2.0 * a) / (sum + 1.0); 
			}
			else {
				a1 = a / sum;
			}
			conflictAffinities.put(c, a1);
		}
	}

	public void processScores() {
		RecursiveNormalizationUtil.processScores(conflictAffinities, 1, true);
	}

}
